﻿using System;
using System.IO;
using System.Net.Sockets;
using System.Windows.Forms;
using ChaterFuncLib;
using MySql.Data.MySqlClient;

namespace ChaterServer
{
    public class Client
    {
        NetworkStream stream;
        public TcpClient client;
        ChattingCore core;
        int id = 0;
        string nick = string.Empty;
        public bool alive = false;
        System.Windows.Forms.Timer connectChechTimeout = new System.Windows.Forms.Timer(), messageReceiverTimeout = new System.Windows.Forms.Timer();

        /// <summary>
        /// Конструктор класс Client
        /// </summary>
        /// <param name="listener">TcpClient от TcpListener (listener.AcceptTcpClient());</param>
        /// <param name="cc">Объект ChattingCore</param>
        public Client(TcpClient client, ChattingCore cc)
        {
            core = cc;
            core.LogMessage("Начинаю принимать подключение...", ChattingCore.LogMessageType.Normal, ChattingCore.LogLevel.Extended);
            this.client = client;

            if (client.Connected)
            {
                core.LogMessage("Соединились!", ChattingCore.LogMessageType.Normal, ChattingCore.LogLevel.Extended);
                stream = client.GetStream();
                EstablishConnection(stream);
            }
            else
            {
                core.LogMessage("Неизвестная ошибка подключения! Соединение будет разорвано!", ChattingCore.LogMessageType.Error, ChattingCore.LogLevel.Extended);
                client.Close();
                return;
            }
            connectChechTimeout.Interval = 1000;
            messageReceiverTimeout.Interval = 50;
        }

        private void CheckConnect(object sender, EventArgs e)
        {
            Packet pkt = new Packet(0, PacketType.SMSG_CHECK_CONNECTION);
            try
            {
                PacketIO.SendPacket(stream, pkt);
            }
            catch (IOException)
            {
                messageReceiverTimeout.Stop();
                connectChechTimeout.Stop();
                alive = false;
                client.Close();
                new MySqlCommand("UPDATE users SET online = 0 WHERE username = '" + nick + "';", core.sqlconn).ExecuteNonQuery();
                core.RemoveFromUsers(this);
                core.BroadcastMessage("[SERVER]", nick + " покинул сеть");
                core.LogMessage("Клиент отключился: " + nick, ChattingCore.LogMessageType.Error, ChattingCore.LogLevel.Standart);
            }
        }

        public void SendMessage(string sender, string message)
        {
            Packet pkt = new Packet(2, PacketType.SMSG_MESSAGE);
            pkt[0] = sender;
            pkt[1] = message;
            PacketIO.SendPacket(stream, pkt);
        }

        public void SendMessage(Packet pkt)
        {
            PacketIO.SendPacket(stream, pkt);
        }

        /// <summary>
        /// Функция обмена сообщениями
        /// </summary>
        private void IOMessenger(object sender, EventArgs e)
        {
            Packet pkt = null;

            if (client.Available != 0)
            {
                if (TryReceivePacket(PacketType.CMSG_MESSAGE, out pkt, stream))
                {
                    core.LogMessage(nick + ": " + pkt[0], ChattingCore.LogMessageType.Normal, ChattingCore.LogLevel.Extended);
                    core.BroadcastMessage(nick, pkt[0].ToString());
                }
            }
        }

        /// <summary>
        /// Функция для установки соединения
        /// </summary>
        /// <param name="stream">Сетевой поток обмена данными</param>
        private void EstablishConnection(NetworkStream stream)
        {
            Packet pkt = null;
            if (TryReceivePacket(PacketType.CMSG_CHECK_CONNECTION, out pkt, stream))
                core.LogMessage("Получен пакет CMSG_CHECK_CONNECTION", ChattingCore.LogMessageType.Normal, ChattingCore.LogLevel.Debug);
            else
            {
                core.LogMessage("Некорректное начало авторизации! Необходим пакет CMSG_CHECK_CONNECTION! Закрытие подключения...", ChattingCore.LogMessageType.Error, ChattingCore.LogLevel.Extended);
                client.Close();
                return;
            }

            if (Service.isClosed())
            {
                core.LogMessage("Сервер закрыт для подключений! Посылаю пакет SMSG_CONNECTION_CLOSED...", ChattingCore.LogMessageType.Warning, ChattingCore.LogLevel.Debug);
                pkt = new Packet(0, PacketType.SMSG_CONNECTION_CLOSED);
                PacketIO.SendPacket(stream, pkt);
                client.Close();
                return;
            }

            core.LogMessage("IP-адрес клиента: " + Service.GetClientIP(client), ChattingCore.LogMessageType.Normal, ChattingCore.LogLevel.Extended);
            core.LogMessage("Посылаю пакет SMSG_CONNECTION_AVAILABLE", ChattingCore.LogMessageType.Normal, ChattingCore.LogLevel.Debug);
            pkt = new Packet(0, PacketType.SMSG_CONNECTION_AVAILABLE);
            PacketIO.SendPacket(stream, pkt);

            if (TryReceivePackets(new PacketType[] { PacketType.CMSG_AUTH, PacketType.CMSG_REGISTER }, out pkt, stream))
            {
                if (pkt == PacketType.CMSG_REGISTER)
                    RegisterClient(stream, pkt);
                else
                    AuthorizeClient(stream, pkt);
            }
            else
            {
                core.LogMessage("Получен пакет, не соответствующий CMSG_[AUTH|REGISTER]... Закрытие подключения...", ChattingCore.LogMessageType.Error, ChattingCore.LogLevel.Extended);
                client.Close();
                return;
            }
        }

        /// <summary>
        /// Функция авторизации
        /// </summary>
        /// <param name="stream">Сетевой поток обмена данными</param>
        /// <param name="authpacket">Пакет CMSG_AUTH</param>
        private void AuthorizeClient(NetworkStream stream, Packet authpacket)
        {
            string pass = string.Empty;
            core.LogMessage("Начинаю авторизацию...", ChattingCore.LogMessageType.Normal, ChattingCore.LogLevel.Extended);
            Packet resultpkt = new Packet(1, PacketType.SMSG_AUTH_ERROR);
            MySqlCommand cmd = new MySqlCommand("SELECT entry, password, PASSWORD('" + authpacket[1] + "'), online FROM users WHERE username = '" + authpacket[0] + "'", core.sqlconn);
            MySqlDataReader reader = cmd.ExecuteReader();
            reader.Read();
            if (!reader.HasRows)
            {
                core.LogMessage("Пользователь не существует!", ChattingCore.LogMessageType.Error, ChattingCore.LogLevel.Extended);
                resultpkt[0] = "Usernotexist";
                PacketIO.SendPacket(stream, resultpkt);
                reader.Close();
                client.Close();
                return;
            }
            id = reader.GetInt32(0);
            pass = reader.GetString(1);
            nick = authpacket[0].ToString();
            if (reader.GetString(2) != pass)
            {
                core.LogMessage("Неверный пароль!", ChattingCore.LogMessageType.Error, ChattingCore.LogLevel.Extended);
                resultpkt[0] = "IncorrectPassword";
                PacketIO.SendPacket(stream, resultpkt);
                cmd = null;
                reader = null;
                client.Close();
                return;
            }
            if (reader.GetInt32(3) == 1)
            {
                core.LogMessage("Пользователь уже в сети: " + nick, ChattingCore.LogMessageType.Error, ChattingCore.LogLevel.Extended);
                resultpkt[0] = "Alreadyonline";
                PacketIO.SendPacket(stream, resultpkt);
                reader.Close();
                client.Close();
                return;
            }
            reader.Close();
            resultpkt = new Packet(0, PacketType.SMSG_AUTH_OK);
            PacketIO.SendPacket(stream, resultpkt);
            cmd.CommandText = "UPDATE users SET lastlogin = '" + DateTime.Now.ToString() + "', online = 1 WHERE username = '" + nick + "'";
            cmd.ExecuteNonQuery();
            core.AddToUsers(this);
            alive = true;
            core.LogMessage("Авторизация пройдена!", ChattingCore.LogMessageType.Normal, ChattingCore.LogLevel.Extended);
            connectChechTimeout.Start();
            connectChechTimeout.Tick += new EventHandler(CheckConnect);
            messageReceiverTimeout.Start();
            messageReceiverTimeout.Tick += new EventHandler(IOMessenger);
        }

        /// <summary>
        /// Функция регистрации
        /// </summary>
        /// <param name="stream">Сетевой поток обмена данными</param>
        /// <param name="regpacket">Пакет CMSG_REGISTER</param>
        private void RegisterClient(NetworkStream stream, Packet regpacket)
        {
            Packet pkt = new Packet(1, PacketType.SMSG_REGISTER_STATUS_INFO);
            MySqlCommand cmd = new MySqlCommand("SELECT * FROM users WHERE username = '" + regpacket[0] + "'", core.sqlconn);
            MySqlDataReader reader = cmd.ExecuteReader();
            reader.Read();
            if (reader.HasRows)
            {
                pkt[0] = "Nickerror";
                PacketIO.SendPacket(stream, pkt);
                core.LogMessage("Попытка регистрации с существующим логином: " + regpacket[0], ChattingCore.LogMessageType.Error, ChattingCore.LogLevel.Extended);
                reader.Close();
                return;
            }
            reader.Close();
            cmd.CommandText = "SELECT * FROM users WHERE email = '" + regpacket[2] + "'";
            reader = cmd.ExecuteReader();
            reader.Read();
            if (reader.HasRows)
            {
                pkt[0] = "Mailerror";
                PacketIO.SendPacket(stream, pkt);
                core.LogMessage("Этот почтовый ящик уже занят: " + regpacket[2], ChattingCore.LogMessageType.Error, ChattingCore.LogLevel.Extended);
                reader.Close();
                return;
            }
            reader.Close();
            cmd.CommandText = "SELECT MAX(entry) FROM users";
            uint id = 0;
            reader = cmd.ExecuteReader();
            reader.Read();
            try
            {
                id = reader.GetUInt32(0);
            }
            catch (System.Data.SqlTypes.SqlNullValueException)
            {
                id = 0;
            }
            reader.Close();
            cmd.CommandText = @"INSERT INTO users VALUES" +
                "(" + (id + 1).ToString() + ", '" +
                regpacket[0] + "', PASSWORD('" +
                regpacket[1] + "'), '" +
                regpacket[2] + "', null, '" +
                Service.GetClientIP(client).ToString() + "', 0);";
            cmd.ExecuteNonQuery();

            pkt[0] = "success";
            PacketIO.SendPacket(stream, pkt);
            core.LogMessage("Регистрация в сети успешна: " + regpacket[0], ChattingCore.LogMessageType.Normal, ChattingCore.LogLevel.Extended);
            return;
        }

        /// <summary>
        /// Попытка принять конкретный пакет
        /// </summary>
        /// <param name="type">Тип пакета</param>
        /// <param name="pkt">Переменная Packet для сохранения</param>
        /// <param name="stream">Поток приема</param>
        /// <returns>True, если принят нужный пакет; False, если пакет другого типа</returns>
        private bool TryReceivePacket(PacketType type, out Packet pkt, NetworkStream stream)
        {
            pkt = null;
            for (; ; )
            {
                Application.DoEvents();
                try
                {
                    if (client.Available != 0)
                    {
                        try
                        {
                            pkt = Serializer.Deserialize(stream);
                            if (pkt != null)
                                break;
                        }
                        catch (SocketException ex)
                        {
                            core.LogMessage("Ошибка сокета: " + ex.Message, ChattingCore.LogMessageType.Error, ChattingCore.LogLevel.Extended);
                            return false;
                        }
                        catch (IOException ex)
                        {
                            core.LogMessage("Ошибка обмена данными с сокетом: " + ex.Message, ChattingCore.LogMessageType.Error, ChattingCore.LogLevel.Extended);
                            client.Close();
                            return false;
                        }
                    }
                }
                catch (ObjectDisposedException) { }
            }
            if (pkt != type)
                return false;
            return true;
        }

        /// <summary>
        /// Попытка принять пакет, который может иметь один из перечисленных типов
        /// </summary>
        /// <param name="types">Массив типов</param>
        /// <param name="pkt">Переменная Packet для сохранения</param>
        /// <param name="stream">Поток приема</param>
        /// <returns>True, если принят нужный пакет; False, если пакет другого типа</returns>
        private bool TryReceivePackets(PacketType[] types, out Packet pkt, NetworkStream stream)
        {
            pkt = null;
            for (; ; )
            {
                Application.DoEvents();
                try
                {
                    if (client.Available != 0)
                    {
                        try
                        {
                            pkt = Serializer.Deserialize(stream);
                            if (pkt != null)
                                break;
                        }
                        catch (SocketException ex)
                        {
                            core.LogMessage("Ошибка сокета: " + ex.Message, ChattingCore.LogMessageType.Error, ChattingCore.LogLevel.Extended);
                            return false;
                        }
                        catch (IOException ex)
                        {
                            core.LogMessage("Ошибка обмена данными с сокетом: " + ex.Message, ChattingCore.LogMessageType.Error, ChattingCore.LogLevel.Extended);
                            client.Close();
                            return false;
                        }
                    }
                }
                catch (ObjectDisposedException) { }
            }

            for (int i = 0; i < types.Length; i++)
            {
                if (pkt != types[i])
                    continue;
                else
                    return true;
            }
            return false;
        }

        public void Stop()
        {
            messageReceiverTimeout.Stop();
            connectChechTimeout.Stop();
        }
    }
}